'  ==========================================================================
'  |                                                                        |
'  | SQLitening Client                                                      |
'  |                                                                        |
'  ==========================================================================
'
'  Contains routines called by SQLitening.Dll to acess remote SQLite databases
'  and flat files.
'
'  ==========================================================================
'  PUBLIC DOMAIN SOFTWARE
'  The author or authors of this code dedicate any and all
'  copyright interest in this code to the public domain.
'  Anyone is free to copy, modify, publish, use, compile,
'  sell, or distribute the original code, either in source
'  code form or as a compiled binary, for any purpose,
'  commercial or non-commercial, and by any means.
'  Fred Meier - July 2010
'  ==========================================================================

'  2011-05-21 DremLIN.Ru - Start modifications
                                  
' For Build project as slsql.dll                                   
'%SLSQL = 1

#If (Not %Def(%SLSQL)) And (Not %Def(%SLSQL_ONLY))
   #Compile Dll "..\Bin\SQLiteningClient.Dll"
   #Dim All
#EndIf

#If %Def(%SLSQL) And (Not %Def(%SLSQL_ONLY))
   #Compile Dll "..\Bin\slsqlite.Dll"
   #Dim All
#EndIf

'============================<[ Equates ]>=============================
' ==========>>> SQLitening return codes
%SQLitening_AccessDenied            =  -8
%SQLitening_InvalidSetNumber        = -14
%SQLitening_SendOrReceiveError      = -18

' ==========>>> SQLite return codes
%SQLITE3_OK               =   0     '*/ Successful result */

' ==========>>> Request Codes
%reqSel                 =  0
%reqExe                 =  1
%reqExeBind             =  2
%reqGetRowDataChunk     =  3
%reqSelRelNamedLocks    =  4
%reqSetBusyTimeout      =  5
%reqGetErrorName        =  6
%reqGetErrorNumber      =  7
%reqGetLastInserRowID   =  8
%reqGetChanges          =  9
%reqGetTotalChanges     = 10
%reqAttach              = 11
%reqClose               = 12
%reqCopyDatabase        = 13
%reqConnect             = 16
%reqOpen                = 17
%reqDisconnect          = 18
%reqGetFile             = 19
%reqPutFile             = 20
%reqRunProc             = 21
%reqGetStatus           = 22

' ==========>>> Misc
%SQLiteningClientVersion   = 140

'============================<[ Declares ]>============================
Declare Function GetComputerName LIB "KERNEL32.DLL" ALIAS "GetComputerNameA" (lpBuffer AS ASCIIZ, nSize AS LONG) AS LONG
Declare Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" (lpBuffer As Asciz, nSize As Long) As Long
Declare Function ZLibCompress Lib "ZLib.dll" Alias "compress"(compr As Any, comprLen As Dword, buf As Any, byval buflen As Dword)As Long
Declare Function ZLibUnCompress Lib "ZLib.dll" Alias "uncompress"(uncompr As Any, uncomprLen As Dword, compr As Any, byval lcompr As Dword)As Long

'============================<[ Globals ]>=============================
Threaded ghSocket as Long           'Server connection socket
Threaded gsaSets() as String        'Array of sets with row chunk
   ' An entry exists for each active set and has the folling format:
   '     1 -   4 = Set number as Long.
   '     5 -   8 = Position of next row as Dword.
   '     9 -  12 = Length of row data chunk as Long.
   '    13 -   R = Row data chunk
   '   R+1 - R+4 = Position next row data chunk as Dword, zero if none

'===========================<[ Do Request ]>===========================
Function DoRequest (byval rlRequest as Long, _
                    byval rhVI1 as Dword, _
                    byval rhVI2 as Dword, _
                    rsSI as String, _
                    whVO1 as Dword, _
                    optional wsSO as String) as Long
'   This rut will send message to server and receive the answer. The
'   answer is broken down into the return code, a Dword, the optional
'   string.
'  Data to server will be fromatted as follows
'      1 -  4  =  Total DataOut lenght as Dword
'      5 -  5  =  Flags as Byte, 1 = Bytes 16 and byound are compressed.
'      6 -  7  =  Request code as Integer
'      8 - 11  =  Value 1. Database or Set handle or other numeric value as Dword
'     12 - 15  =  Value 2. Column number or other numeric value as Dword
'     16 -  ?  =  Data. Format depends on request.
'
' Data from server will be fromatted as follows
'      1 -  4  =  Total DataIn lenght as Dword
'      5 -  5  =  Flags as Byte, 1 = Bytes 12 and byound are compressed.
'      6 -  7  =  Return code as Integer normally from the SQLite engine.
'      8 - 11  =  Value 1 as Dword.
'     12 -  ?  =  Data. Format depends on request.
'

   Local llA, llB as Long
   Local lsA as String
   Local lsDataIn as String
   Dim lsaDataIn(0 to 4) as String

   ' Send request to server, compressing if needed
   if len(rsSI) > 100 then
      ' Compressing
      lsA = space$(len(rsSI) * 1.2 + 12)
      llA = len(lsA)
      ZLibCompress byval strptr(lsA), llA, byval strptr(rsSI), len(rsSI)
      ' Now the returning message is made up of ...
      '     -- first 5 bytes are total length for 4 and flag for 1
      '     -- next 10 bytes are the request and the 2 values
      '     -- last n bytes are lenght of uncompressed data for 4 and then the compressed data
      tcp send ghSocket, mkdwd$(llA + 19) & mkbyt$(1) & mki$(rlRequest) & mkdwd$(rhVI1) & mkdwd$(rhVI2) & mkdwd$(len(rsSI)) & left$(lsA, llA)
   else
      ' No Compressing
      tcp send ghSocket, mkdwd$(len(rsSI) + 15) & mkbyt$(0) & mki$(rlRequest) & mkdwd$(rhVI1) & mkdwd$(rhVI2) & rsSI
   end if
   ' Check for error
   if err then goto ErrorOccured    'Could not send request Probably not connected.

   ' Receive first chunk of data
   do
      sleep 15
      tcp recv ghSocket, 1024 * 32, lsaDataIn(0)
      if err = 24 then ErrorOccured
   loop while isfalse len(lsaDataIn(0)) and isfalse err and isfalse eof(ghSocket)

   ' Receive any remaining chunks
   llA = cvdwd(lsaDataIn(0))
   if len(lsaDataIn(0)) < llA then

      ' There is more data
      llA = llA - len(lsaDataIn(0))
      do
         incr llB
         if llB > ubound(lsaDataIn) then redim preserve lsaDataIn(0 to llB + 5)
         tcp recv ghSocket, llA, lsaDataIn(llB)
         llA = llA - len(lsaDataIn(llB))
      loop while llA > 0 and len(lsaDataIn(llB))
      if llA <> 0 then goto ErrorOccured     'Received wrong length

   elseif isfalse llA then

      ' There was no data in message so we have a problem Houston.
      ' If this was a Connect request then will assume it will be
      ' caught by Connect rut.
      if rlRequest <> %reqConnect then goto ErrorOccured       ' Received nothing

   end if
   if err then goto ErrorOccured       ' Could not recieve request  Probably not connected
   lsDataIn = join$(lsaDataIn(), "")

   ' Build returning stuff, uncompressing if needed
   if cvbyt(lsDataIn, 5) = 1 then
      ' Uncompress it
      lsA = space$(cvdwd(lsDataIn, 6))
      ZLibUnCompress byval strptr(lsA), len(lsA), byval strptr(lsDataIn) + 9, len(lsDataIn) - 9
      function = cvi(lsA, 1)
      whVO1 = cvdwd(lsA, 3)
      if varptr(wsSO) then wsSO = mid$(lsA, 7)
   else
      function = cvi(lsDataIn, 6)
      whVO1 = cvdwd(lsDataIn, 8)
      if varptr(wsSO) then wsSO = mid$(lsDataIn, 12)
   end if

   ' Exit
   exit function

   ErrorOccured:
   function = %SQLitening_SendOrReceiveError

End Function

'=============================<[ Attach ]>=============================
Function SQLiteAttach alias "SQLiteAttach" (byval rhDab as Dword, _
                                            rsFileName as String, _
                                            rsAsName as String) Export as Long
   function = DoRequest(%reqAttach, rhDab, 0, rsFileName  & $VT & rsAsName, 0)
End Function

'=============================<[ Close ]>==============================
Function sqlite3_close Cdecl alias "sqlite3_close" (byval rhDab as Dword) Export as Dword
   function = DoRequest(%reqClose, rhDab, 0, "", 0)
End Function

'============================<[ Connect ]>=============================
Function SQLiteConnect alias "SQLiteConnect" (rsServer as String, _
                                              byval rlPort as Long, _
                                              rsModChars as String, _
                                              wsOutData as String) Export as Long
'   ModChars:   Tn = Where n is the how long a TCP SEND/ RECV should
'                    wait for completion, in milliseconds (mSec). The
'                    default timeout is 30000 milliseconds (30 seconds).
'                    This wait should be set to be at least 3 times as
'                    long as the wait for a database lock which that
'                    default is 10000 milliseconds (10 seconds) and is
'                    set in slOpen and can be changed in slSetProcessMods.
'               u  = Do not pass user name.
'               c  = Do not pass computer name.
'               i  = Do not pass IP address.
'
   Local llA as Long
   Local llRC as Long
   Local lbpA as Byte Ptr
   Local lzA as Asciz * 4096
   local lsA as String

   ghSocket = freefile
   llA = instr(rsModChars, "T")
   tcp open port iif&(rlPort, rlPort, 51234) at rsServer as ghSocket timeout iif&(llA, val(mid$(rsModChars, llA + 1)), 30000)
   function = err
   if err then exit function

   ' No PB open error so send connect message to see if we
   ' can "talk" to the SQLitening service.  If the returning
   ' Value 1 is -1 then we talked OK.  If not then if return code
   ' is zero then  return %SQLitening_AccessDenied -- Access denied.
   if isfalse instr(rsModChars, "u") then
      GetUserName lzA, sizeof(lzA)
      lsA = remove$(lzA, any $BS & $VT)
   end if
   if isfalse instr(rsModChars, "c") then
      GetComputerName lzA, sizeof(lzA)
   else
      reset lzA
   end if
   lsA = lsA & $BS & remove$(lzA, any $BS & $VT)
   host addr "" to llA
   if isfalse instr(rsModChars, "i") then
      lbpA = varptr(llA)
      lzA = using$("#_.#_.#_.#", @lbpA, @lbpA[1], @lbpA[2], @lbpA[3])
   else
      reset lzA
   end if
   lsA = lsA & $BS & lzA
   llRC = DoRequest(%reqConnect, %SQLiteningClientVersion, 0, lsA, llA, wsOutData)
   if llA <> -1 then function = iif&(llRC, llRc, %SQLitening_AccessDenied)

End Function

'===========================<[ Disconnect ]>===========================
Sub SQLiteDisconnect alias "SQLiteDisconnect" Export
   DoRequest %reqDisconnect, 0, 0, "", 0
End Sub

'==============================<[ Exe ]>===============================
Function sqlite3_exec Cdecl alias "sqlite3_exec" (byval rhDab as Dword, _
                                                  byval rspStatements as Asciz Ptr, _
                                                  byval rhZero1 as Dword, _
                                                  byval rlFlags as Long, _
                                                  byval rhZero3 as Dword) Export as Long
   function = DoRequest(%reqExe, rhDab, rlFlags, bycopy @rspStatements, 0)
End Function

'============================<[ Exe Bind ]>============================
Function SQLiteExeBind alias "SQLiteExeBind" (byval rhDab as Dword, _
                                              rsStatement as String, _
                                              rsData as String, _
                                              byval rlFlags as Long, _
                                              byval rlVarRowCount as Long)Export as Long
   ' $NUL is concatenated cause there is a small performance advantage to be had by passing
   ' the input string including the nul-terminator byte. Len is also + 1.
   function = DoRequest(%reqExeBind, rhDab, rlFlags, mkl$(rlVarRowCount) & mkdwd$(len(rsStatement) + 1) & rsStatement & $NUL & rsData, 0)
End Function

'===========================<[ Close Set ]>============================
Function SQLiteCloseSet alias "SQLiteCloseSet" (byval rlSetNumber as Long) Export as Long
   Local llSetIX as Long
   array scan gsaSets(), from 1 to 4, =  mkl$(rlSetNumber), to llSetIX
   if llSetIX then reset gsaSets(llSetIX) else function = %SQLitening_InvalidSetNumber
End Function

'=========================<[ Copy Database ]>==========================
Function SQLiteCopyDatabase alias "SQLiteCopyDatabase" (byval rhDab as Dword, _
                                                        byval rhDestDab as Dword, _
                                                        rsDatabaseName as String, _
                                                        byval rlPages as Long, _
                                                        byval rlSleep as Long)Export as Long
   function = DoRequest(%reqCopyDatabase, _
                        rhDab, _
                        rhDestDab, _
                        mkl$(rlPages) & mkl$(rlSleep) & rsDatabaseName, 0)
End Function

'==========================<[ Get Changes ]>===========================
Function sqlite3_changes Cdecl alias "sqlite3_changes" (byval rhDab as Dword) Export as long
   function = DoRequest(%reqGetChanges, rhDab, 0, "", 0)
End Function

'========================<[ Get Column Text ]>=========================
Function SQLiteGetColumnText alias "SQLiteGetColumnText" (byval rlSetNumber as Long, _
                                                          byval rlCol as Long) Export as String
'   Returns the text data for passed SetNumber at passed Col
'   Row data chunk in array format:
'        1 -   4 = Set number as Long.
'        5 -   8 = Position of next row as Dword.
'        9 -  12 = Length of row data chunk.
'       13 -   R = Row data chunk
'      R+1 - R+4 = Position next row data chunk as Dword, zero if none

   Local llSetIX as Long
   Local llColCount as Long
   Local llColLen as Long
   Local lhColPos as Dword
   Local lhEndOfRow as Dword

   ' Find set
   array scan gsaSets(), from 1 to 4, = mkl$(rlSetNumber), to llSetIX
   if llSetIX then

      ' Init stuff
      lhColPos = cvdwd(gsaSets(llSetIX), 5) + 4
      lhEndOfRow = lhColPos + cvdwd(gsaSets(llSetIX), lhColPos - 4)

      ' Locate column
      do while lhColPos < lhEndOfRow
         llColLen = cvbyt(gsaSets(llSetIX), lhColPos)
         incr lhColPos
         if llColLen >= 254 then
            if llColLen = 255 then
               llColLen = cvdwd(gsaSets(llSetIX), lhColPos)
               lhColPos = lhColPos + 4
            elseif llColLen = 254 then
               reset llColLen
            end if
         end if

         ' Check if requested column
         if llColCount = rlCol then
            function = mid$(gsaSets(llSetIX), lhColPos, llColLen)
            exit do
         end if

         ' Bump to next col
         lhColPos = lhColPos + llColLen
         incr llColCount
      loop
   end if

End Function

'========================<[ Get Colunm Type ]>=========================
Function SQLiteGetColumnType alias "SQLiteGetColumnType" (byval rlSetNumber as Long, _
                                                          byval rlCol as Long) Export as Dword
'   Will return 5 if column is NULL or 0(unknown) if not
'   Row data chunk in array format:
'        1 -   4 = Set number as Long.
'        5 -   8 = Position of next row as Dword.
'        9 -  12 = Length of row data chunk.
'       13 -   R = Row data chunk
'      R+1 - R+4 = Position next row data chunk as Dword, zero if none

   Local llSetIX as Long
   Local llColCount as Long
   Local llColLen as Long
   Local llColType as Long
   Local lhColPos as Dword
   Local lhEndOfRow as Dword

   ' Find set
   array scan gsaSets(), from 1 to 4, = mkl$(rlSetNumber), to llSetIX
   if llSetIX then

      ' Init stuff
      lhColPos = cvdwd(gsaSets(llSetIX), 5) + 4
      lhEndOfRow = lhColPos + cvdwd(gsaSets(llSetIX), lhColPos - 4)

      ' Locate column
      do while lhColPos < lhEndOfRow
         llColType = 9
         llColLen = cvbyt(gsaSets(llSetIX), lhColPos)
         incr lhColPos
         if llColLen = 254 then
            llColType = 5
            reset llColLen
         elseif llColLen = 255 then
            llColLen = cvdwd(gsaSets(llSetIX), lhColPos)
            lhColPos = lhColPos + 4
         end if

         ' Check if requested column
         if llColCount = rlCol then
            function = llColType
            exit do
         end if

         ' Bump to next col
         lhColPos = lhColPos + llColLen
         incr llColCount
      loop
   end if

End Function

'=========================<[ Get Error Name ]>=========================
Function sqlite3_errmsg Cdecl alias "sqlite3_errmsg" (byval rhDab as Dword) Export as String
   Local lsA as String
   DoRequest %reqGetErrorName, rhDab, 0, "", 0, lsA
   function = lsA
End Function

'========================<[ Get Error Number ]>========================
Function sqlite3_errcode Cdecl alias "sqlite3_errcode" (byval rhDab as Dword) Export as Long
   function = DoRequest(%reqGetErrorNumber, rhDab, 0, "", 0)
End Function

'============================<[ Get File ]>============================
Function SQLiteGetFile alias "SQLiteGetFile" (rsFileName as String, _
                                              wsFileData as String, _
                                              rsModChars as String)Export as Long
   function = DoRequest(%reqGetFile, iif&(instr(rsModChars, "S"), 1, 0), 0, rsFileName, 0, wsFileData)
End Function

'===========================<[ Get Status ]>===========================
Function SQLiteGetStatus alias "SQLiteGetStatus" (byval rhDab as Dword, _
                                                   byval rlRequest as Long, _
                                                   wsStatus as String) Export as Long
   function = DoRequest(%reqGetStatus, rhDab, rlRequest, "", 0, wsStatus)
End Function

'============================<[ Put File ]>============================
Function SQLitePutFile alias "SQLitePutFile" (rsFileName as String, _
                                              rsFileData as String, _
                                              rsModChars as String)Export as Long
   function = DoRequest(%reqPutFile, _
                        iif&(instr(rsModChars, "S"), 1, 0) + iif&(instr(rsModChars, "C"), 2, 0) + iif&(instr(rsModChars, "D"), 4, 0) + iif&(instr(rsModChars, "T"), 8, 0), _
                        0, rsFileName & $BEL & rsFileData, 0)
End Function

'========================<[ Get Insert Rowid ]>========================
Function sqlite3_last_insert_rowid Cdecl alias "sqlite3_last_insert_rowid" (byval rhDab as Dword) Export as Quad
   Local lsRowID as String
   DoRequest %reqGetLastInserRowID, rhDab, 0, "", 0, lsRowID
   function = val(lsRowID)
End Function

'=======================<[ Get Total Changes ]>========================
Function sqlite3_total_changes Cdecl alias "sqlite3_total_changes" (byval rhDab as Dword) Export as Long
   function = DoRequest(%reqGetTotalChanges, rhDab, 0, "", 0)
End Function

'===========================<[ Next Row ]>============================
Function SQLiteNextRow alias "SQLiteNextRow" (byval rhDab as Dword, _
                                              byval rlSetNumber as Long) Export as Long
'   Bumps to next row within the row data chunk which is stored in the
'   global sets array.  If no more rows in current row data chunk and
'   there are more rows at server then will request next row data chunk
'   from server.
'   Returns -1 if there is a next row or 0 if no row available any other
'   value is an error.
'   Row data chunk in array format:
'        1 -   4 = Set number as Long.
'        5 -   8 = Position of next row as Dword.
'        9 -  12 = Length of row data chunk.
'       13 -   R = Row data chunk
'      R+1 - R+4 = Position next row data chunk as Dword, zero if none
'   Row data chunk returned from server format:
'        1 -   4 = Length of row data chunk.
'        5 -   R = Row data chunk
'      R+1 - R+4 = Position next row data chunk as Dword, zero if none

   Local llRC as Long
   Local llSetIX as Long
   Local lhCurRowPos as Dword
   Local lsRowDataChunk as String

   ' Find set
   array scan gsaSets(), from 1 to 4, = mkl$(rlSetNumber), to llSetIX
   if llSetIX then

      ' Set ptr to first row or bump to next
      lhCurRowPos = cvdwd(gsaSets(llSetIX), 5)
      if lhCurRowPos then
         ' Bump to next
         lhCurRowPos = lhCurRowPos + cvdwd(gsaSets(llSetIX), lhCurRowPos) + 4
      else
         ' Set to first
         lhCurRowPos =13
      end if

      ' Check for last row
      if lhCurRowPos >= cvdwd(gsaSets(llSetIX), 9) + 13 then

         ' Check if there are more rows available
         if cvdwd(gsaSets(llSetIX), lhCurRowPos) then

            ' Get next row data chunk
            llRC = DoRequest(%reqGetRowDataChunk, rhDab, rlSetNumber, mid$(gsaSets(llSetIX), lhCurRowPos), 0, lsRowDataChunk)
            if llRC <> -1 then function = llRC : goto NoMoreRowsAvailable
            gsaSets(llSetIX) = mkl$(rlSetNumber) & mkdwd$(13) & lsRowDataChunk

         else

            ' No more rows available or error so empty the entry for this set and exit function returning non -1
            NoMoreRowsAvailable:
            reset gsaSets(llSetIX)
            exit function

         end if

      else

         ' Update current row position
         mid$(gsaSets(llSetIX), 5) = mkdwd$(lhCurRowPos)

      end if

      ' We are now pointing to next row so return -1
      function = -1

   end if

End Function

'==============================<[ Open ]>==============================
Function sqlite3_open_v2 Cdecl alias "sqlite3_open_v2" (zFilename as Asciiz, _
                                                        whDab as Dword, _
                                                        byval rlFlags as Long, _
                                                        byval rlNull as Long) Export as Long
   function = DoRequest(%reqOpen, rlFlags, 0, bycopy zfilename, whDab)
End Function

'============================<[ Run Proc ]>============================
Function SQLiteRunProc alias "SQLiteRunProc" (byval rhDab as Dword, _
                                              rsProcName as String, _
                                              blParm1 as Long, _
                                              blParm2 as Long, _
                                              bsParm3 as String, _
                                              bsParm4 as String, _
                                              rsModChars as String)Export as Long
'   String to be passed is as follows:
'       1 -  4 = blParm1
'       5 -  8 = blParm2
'       9 - 12 = length of rsProcName
'      13 - ?? = rsProcName
'      ?? - ?? = length of bsParm3
'      ?? - ?? = bsParm3
'      ?? - ?? = length of bsParm4
'      ?? - ?? = bsParm4
'   String to be returned is as follows:
'       1 -  4 = blParm1
'       5 -  8 = blParm2
'       9 - ?? = length of bsParm3
'      ?? - ?? = bsParm3
'      ?? - ?? = length of bsParm4
'      ?? - ?? = bsParm4

   Local llOffset, llLength as Long
   Local lsA as String

   ' Call proc on server
   function = DoRequest(%reqRunProc, rhDab, _
                        iif&(instr(rsModChars, "L"), 1, 0) + iif&(instr(rsModChars, "U"), 2, 0) +  + iif&(instr(rsModChars, "u"), 4, 0), _
                        mkl$(blParm1) & mkl$(blParm2) & mkl$(len(rsProcName)) & rsProcName & _
                        mkl$(len(bsParm3)) & bsParm3 & mkl$(len(bsParm4)) & bsParm4, _
                        0, lsA)

   ' Set returning values from returning string
   blParm1 = cvl(lsA, 1)
   blParm2 = cvl(lsA, 5)
   llOffset = 9
   llLength = cvl(lsA, llOffset)
   bsParm3 = mid$(lsA, llOffSet + 4, llLength)
   llOffset = llOffset + llLength + 4
   llLength = cvl(lsA, llOffset)
   bsParm4 = mid$(lsA, llOffSet + 4, llLength)

End Function

'==============================<[ Sel ]>===============================
Function SQLiteSel alias "SQLiteSel" (byval rhDab as Dword, _
                                      rsStatement as String, _
                                      byval rlSetNumber as Long, _
                                      rsModChars as String, _
                                      wsColumnNames as String) Export as Long
'   Calls the server passing the database handle, set number, statement,
'   and modchars.  Server returns the following in lsData:
'      1 - 4   = Length of column names data
'      5 - N   = Column names data
'    N+1 - N+4 = Length of first chunk of row data
'    N+5 - R   = First chunk of row data
'    R+1 - R+4 = Position next row data chunk as Dword, zero if none

   Local llRC as Long
   Local llSetIX as Long
   Local lsData as String

   llRC = DoRequest(%reqSel, rhDab, rlSetNumber, mkl$(len(rsStatement)) & rsStatement & mkl$(len(rsModChars)) & rsModChars, 0, lsData)
   function = llRC
   if llRC = %SQLITE3_OK then

      ' Return column names
      wsColumnNames = mid$(lsData, 5, cvdwd(lsData))

      ' Add an entry into the setsrows array if there are any rows returned
      if len(mid$(lsData, cvdwd(lsData) + 5)) then

         ' Find an empty sets array element
         array scan gsaSets(), = "", to llSetIX
         if isfalse llSetIX then

            ' No empty entries so add some and set SetIX to first one
            llSetIX = iif&(ubound(gsaSets) > 0, ubound(gsaSets) + 1, 1)
            redim preserve gsaSets(1 to llSetIX + 9)

         end if

         ' Set the set data
         gsaSets(llSetIX) = mkl$(rlSetNumber) & mkdwd$(0) & mid$(lsData, cvdwd(lsData) + 5)

      end if

   end if

End Function

'========================<[ Set Busy Timeout ]>========================
Sub sqlite3_busy_timeout Cdecl alias "sqlite3_busy_timeout" (byval rhDab as Dword, _
                                                             byval rlWaitMS as Long) Export
   DoRequest %reqSetBusyTimeout, rhDab, rlWaitMS, "", 0

End Sub

'===========================<[ Set Rel Named Locks ]>===========================
Function SQLiteSetRelNamedLocks alias "SQLiteSetRelNamedLocks" (byval rhDab as Dword, _
                                                                byref rsLockNames as String, _
                                                                byval rsModChars as String, _
                                                                byval rsSelStatement as String, _
                                                                byval rlSelSetNumber as Long, _
                                                                byval rsSelModChars as String, _
                                                                wsColumnNames as String) Export as Long

'   Calls the server passing the database handle, set number, and string formatted as follows
'       1 -  4 = length of rsLockNames
'       5 - ?? = rsLockNames
'      ?? - ?? = length of rsModChars
'      ?? - ?? = rsModChars
'      ?? - ?? = length of rsSelStatement
'      ?? - ?? = rsSelStatement
'      ?? - ?? = length of rsSelModChars
'      ?? - ?? = rsSelModChars
'   If there was a select statement then the server returns the following in lsData:
'      1 - 4   = Length of column names data
'      5 - N   = Column names data
'    N+1 - N+4 = Length of first chunk of row data
'    N+5 - R   = First chunk of row data
'    R+1 - R+4 = Position next row data chunk as Dword, zero if none

   Local llRC as Long
   Local llSetIX as Long
   Local lsData as String

   ' Do request
   llRC = DoRequest(%reqSelRelNamedLocks, rhDab, rlSelSetNumber, _
                    mkl$(len(rsLockNames)) & rsLockNames & mkl$(len(rsModChars)) & rsModChars & mkl$(len(rsSelStatement)) & rsSelStatement & mkl$(len(rsSelModChars)) & rsSelModChars, _
                    0, lsData)

   function = llRC

   if len(rsSelStatement) and  llRC = %SQLITE3_OK then

      ' Return column names
      wsColumnNames = mid$(lsData, 5, cvdwd(lsData))

      ' Add an entry into the setsrows array if there are any rows returned
      if len(mid$(lsData, cvdwd(lsData) + 5)) then

         ' Find an empty sets array element
         array scan gsaSets(), = "", to llSetIX
         if isfalse llSetIX then

            ' No empty entries so add some and set SetIX to first one
            llSetIX = iif&(ubound(gsaSets) > 0, ubound(gsaSets) + 1, 1)
            redim preserve gsaSets(1 to llSetIX + 9)

         end if

         ' Set the set data
         gsaSets(llSetIX) = mkl$(rlSelSetNumber) & mkdwd$(0) & mid$(lsData, cvdwd(lsData) + 5)

      end if
   end if

End Function


